package com.nbcio.iot.protocol.modbus;

import com.nbcio.iot.config.IotModbusProperties;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusProtocolException;
import com.intelligt.modbus.jlibmodbus.master.ModbusMaster;
import com.intelligt.modbus.jlibmodbus.master.ModbusMasterFactory;
import com.intelligt.modbus.jlibmodbus.serial.SerialParameters;
import com.intelligt.modbus.jlibmodbus.serial.SerialPort;
import com.intelligt.modbus.jlibmodbus.serial.SerialPortFactoryJSSC;
import com.intelligt.modbus.jlibmodbus.serial.SerialUtils;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.InetAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

@Slf4j
@Component
@RequiredArgsConstructor
public class ModbusManager {

    private final IotModbusProperties properties;

    private final ConcurrentMap<String, ModbusMaster> connectionCache = new ConcurrentHashMap<>();

    public Number readHoldingRegisterTcp(int slaveId, int offset) throws Exception {
        IotModbusProperties.Tcp cfg = properties.getTcp();
        return readHoldingRegisterTcp(cfg.getHost(), cfg.getPort(), cfg.getTimeoutMs(), slaveId, offset);
    }

    public Number readHoldingRegisterRtu(int slaveId, int offset) throws Exception {
        IotModbusProperties.Rtu cfg = properties.getRtu();
        return readHoldingRegisterRtu(cfg.getPortName(), cfg.getBaudRate(), cfg.getDataBits(), cfg.getStopBits(), cfg.getParity(), cfg.getTimeoutMs(), slaveId, offset);
    }

    public Number readHoldingRegisterTcp(String host, int port, int timeoutMs, int slaveId, int offset) throws Exception {
        String key = "tcp:" + host + ":" + port;
        ModbusMaster master = connectionCache.computeIfAbsent(key, k -> {
            try {
                TcpParameters tcp = new TcpParameters();
                tcp.setHost(InetAddress.getByName(host));
                tcp.setPort(port);
                tcp.setKeepAlive(true);
                ModbusMaster m = ModbusMasterFactory.createModbusMasterTCP(tcp);
                m.setResponseTimeout(timeoutMs);
                return m;
            } catch (Exception e) {
                log.error("create tcp master failed {}:{}", host, port, e);
                return null;
            }
        });
        if (master == null) throw new IllegalStateException("TCP master create failed for " + key);
        try {
            if (!master.isConnected()) master.connect();
            int[] data = master.readHoldingRegisters(slaveId, offset, 1);
            return data != null && data.length > 0 ? data[0] : null;
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            connectionCache.remove(key);
            throw e;
        }
    }

    public Number readHoldingRegisterRtu(String portName, int baudRate, int dataBits, int stopBits, int parity, int timeoutMs, int slaveId, int offset) throws Exception {
        String key = "rtu:" + portName + ":" + baudRate + ":" + dataBits + ":" + stopBits + ":" + parity;
        ModbusMaster master = connectionCache.computeIfAbsent(key, k -> {
            try {
                SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
                SerialParameters sp = new SerialParameters();
                sp.setDevice(portName);
                sp.setBaudRate(mapBaudRate(baudRate));
                sp.setDataBits(dataBits);
                sp.setStopBits(stopBits);
                sp.setParity(mapParity(parity));
                ModbusMaster m = ModbusMasterFactory.createModbusMasterRTU(sp);
                m.setResponseTimeout(timeoutMs);
                return m;
            } catch (Exception e) {
                log.error("create rtu master failed {}", portName, e);
                return null;
            }
        });
        if (master == null) throw new IllegalStateException("RTU master create failed for " + key);
        try {
            if (!master.isConnected()) master.connect();
            int[] data = master.readHoldingRegisters(slaveId, offset, 1);
            return data != null && data.length > 0 ? data[0] : null;
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            connectionCache.remove(key);
            throw e;
        }
    }

    public void writeHoldingRegisterTcp(String host, int port, int timeoutMs, int slaveId, int offset, int value) throws Exception {
        String key = "tcp:" + host + ":" + port;
        ModbusMaster master = connectionCache.computeIfAbsent(key, k -> {
            try {
                TcpParameters tcp = new TcpParameters();
                tcp.setHost(InetAddress.getByName(host));
                tcp.setPort(port);
                tcp.setKeepAlive(true);
                ModbusMaster m = ModbusMasterFactory.createModbusMasterTCP(tcp);
                m.setResponseTimeout(timeoutMs);
                return m;
            } catch (Exception e) {
                log.error("create tcp master failed {}:{}", host, port, e);
                return null;
            }
        });
        if (master == null) throw new IllegalStateException("TCP master create failed for " + key);
        try {
            if (!master.isConnected()) master.connect();
            master.writeSingleRegister(slaveId, offset, value);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            connectionCache.remove(key);
            throw e;
        }
    }

    // backward-compatible overload
    public void writeHoldingRegisterTcp(int slaveId, int offset, int value) throws Exception {
        IotModbusProperties.Tcp cfg = properties.getTcp();
        writeHoldingRegisterTcp(cfg.getHost(), cfg.getPort(), cfg.getTimeoutMs(), slaveId, offset, value);
    }

    public void writeHoldingRegisterRtu(String portName, int baudRate, int dataBits, int stopBits, int parity, int timeoutMs, int slaveId, int offset, int value) throws Exception {
        String key = "rtu:" + portName + ":" + baudRate + ":" + dataBits + ":" + stopBits + ":" + parity;
        ModbusMaster master = connectionCache.computeIfAbsent(key, k -> {
            try {
                SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
                SerialParameters sp = new SerialParameters();
                sp.setDevice(portName);
                sp.setBaudRate(mapBaudRate(baudRate));
                sp.setDataBits(dataBits);
                sp.setStopBits(stopBits);
                sp.setParity(mapParity(parity));
                ModbusMaster m = ModbusMasterFactory.createModbusMasterRTU(sp);
                m.setResponseTimeout(timeoutMs);
                return m;
            } catch (Exception e) {
                log.error("create rtu master failed {}", portName, e);
                return null;
            }
        });
        if (master == null) throw new IllegalStateException("RTU master create failed for " + key);
        try {
            if (!master.isConnected()) master.connect();
            master.writeSingleRegister(slaveId, offset, value);
        } catch (ModbusIOException | ModbusNumberException | ModbusProtocolException e) {
            connectionCache.remove(key);
            throw e;
        }
    }

    // backward-compatible overload
    public void writeHoldingRegisterRtu(int slaveId, int offset, int value) throws Exception {
        IotModbusProperties.Rtu cfg = properties.getRtu();
        writeHoldingRegisterRtu(cfg.getPortName(), cfg.getBaudRate(), cfg.getDataBits(), cfg.getStopBits(), cfg.getParity(), cfg.getTimeoutMs(), slaveId, offset, value);
    }

    public void clearConnectionCache() {
        connectionCache.values().forEach(m -> {
            try { if (m != null && m.isConnected()) m.disconnect(); } catch (Exception ignore) {}
        });
        connectionCache.clear();
    }

    private SerialPort.BaudRate mapBaudRate(int baud) {
        for (SerialPort.BaudRate br : SerialPort.BaudRate.values()) {
            if (br.getValue() == baud) {
                return br;
            }
        }
        return SerialPort.BaudRate.BAUD_RATE_9600;
    }

    private SerialPort.Parity mapParity(int parity) {
        switch (parity) {
            case 1:
                return SerialPort.Parity.ODD;
            case 2:
                return SerialPort.Parity.EVEN;
            default:
                return SerialPort.Parity.NONE;
        }
    }
}